<?php

namespace backend\components;

use Yii;
use backend\models\Extensions;
use yii\helpers\FileHelper;

/**
 * Abstract adapter for the installer.
 *
 * @author loong
 */
abstract class InstallerAdapter
{
    /**
     * ID for the currently installed extension if present
     * @var    integer
     */
    protected $currentExtensionId = null;

    /**
     * Name of the extension
     * @var    string
     */
    protected $name = null;

    /**
     * The unique identifier for the extension (e.g. mod_login)
     * @var string
     */
    protected $element = null;

    /**
     * The type of adapter in use
     * @var    string
     */
    protected $type;

    /**
     * Extension object.
     * @var    Extension
     * */
    protected $extension = null;

    /**
     * Parent
     * @var Object
     */
    protected $parent = null;

    /**
     * Configuration Options
     * @var array
     */
    protected $options = [];

    /**
     * Copy of the XML manifest file.
     * @var string
     */
    public $manifest = null;

    /**
     * Constructor
     * @param Installer $parent Parent object
     * @param array $options Configuration Options
     * @throws \ReflectionException
     */
    public function __construct(Installer $parent, $options = [])
    {
        $this->parent = $parent;
        $this->options = $options;
        $this->init();
    }

    /**
     * 初始化
     * @throws \ReflectionException
     */
    public function init()
    {
        if (!empty($this->options)) {
            $this->setProperties($this->options);
        }
        $this->extension = new Extensions();
        $reflection = new \ReflectionClass(get_called_class());
        $this->type = strtolower(str_replace('Adapter', '', $reflection->getShortName()));
    }

    /**
     * 扩展安装
     * @return bool
     * @throws \Throwable
     * @throws \yii\base\ErrorException
     * @throws \yii\db\Exception
     */
    public function install()
    {
        $manifest = $this->getManifest();
        $description = (string)$manifest->description;
        $this->name = (string)$manifest->name;
        if ($description) {
            $manifestCache = $this->parent->generateManifestCache();
            $isLangCat = (isset($manifestCache['langCat']) && !empty($manifestCache['langCat']));
            $this->parent->message = $isLangCat ? Yii::t($manifestCache['langCat'], $description) : $description;
        } else {
            $this->parent->message = '';
        }
        $this->element = $this->getElement();

        try {
            $this->setupInstallPaths();
        } catch (\RuntimeException $re) {
            $this->parent->abort($re->getMessage());
            return false;
        }

        try {
            $this->checkExistingExtension();
        } catch (\RuntimeException $re) {
            $this->parent->abort($re->getMessage());
            return false;
        }

        try {
            $this->checkExtensionInFilesystem();
        } catch (\RuntimeException $re) {
            $this->parent->abort($re->getMessage());
            return false;
        }

        if ($this->route === 'update') {
            try {
                $this->setupUpdates();
            } catch (\RuntimeException $re) {
                $this->parent->abort($re->getMessage());
                return false;
            }
        }

        try {
            $this->createExtensionRoot();
        } catch (\yii\base\Exception $ex) {
            $this->parent->abort($ex->getMessage());
            return false;
        }

//        try {
//            FileHelper::createDirectory($this->parent->getPath('extension_backend'));
//        } catch (\yii\base\Exception $ex) {
//            $this->parent->pushStep(['type' => 'folder', 'path' => $this->parent->getPath('extension_backend')]);
//            $this->parent->abort($ex->getMessage());
//            return false;
//        }

        try {
            $this->copyBaseFiles();
        } catch (\yii\base\Exception $ex) {
            $this->parent->abort($ex->getMessage());
            return false;
        }

        $this->parseOptionalTags();

        try {
            $this->storeExtension();
        } catch (\RuntimeException $re) {
            $this->parent->abort($re->getMessage());
            return false;
        }

        try {
            $this->parseQueries();
        } catch (\RuntimeException $re) {
            // Install failed, roll back changes
            $this->parent->abort($re->getMessage());
            return false;
        }

        try {
            $this->finaliseInstall();
        } catch (\RuntimeException $re) {
            // Install failed, roll back changes
            $this->parent->abort($re->getMessage());
            return false;
        }
        return $this->extension->id;
    }

    /**
     * Get the manifest object.
     * @return \SimpleXMLElement  Manifest object
     */
    public function getManifest()
    {
        return $this->manifest;
    }

    /**
     * Get the filtered extension element from the manifest
     * @param string $element Optional element name to be converted
     * @return string The filtered element
     */
    public function getElement($element = null)
    {
        if (!$element) {
            // Ensure the element is a string
            $element = (string)$this->getManifest()->element;
        }
        if (!$element) {
            $element = (string)$this->getManifest()->name;
        }

        return $element;
    }

    /**
     * Set the object properties based on a named array/hash.
     * @param mixed $properties Either an associative array or another object.
     * @return boolean
     */
    public function setProperties($properties)
    {
        if (is_array($properties) || is_object($properties)) {
            foreach ((array)$properties as $k => $v) {
                // Use the set function which might be overridden.
                $this->set($k, $v);
            }

            return true;
        }

        return false;
    }

    /**
     * Modifies a property of the object, creating it if it does not already exist.
     * @param string $property The name of the property.
     * @param mixed $value The value of the property to set.
     * @return mixed Previous value of the property.
     */
    public function set($property, $value = null)
    {
        $previous = isset($this->$property) ? $this->$property : null;
        $this->$property = $value;

        return $previous;
    }

    /**
     * Method to do any prechecks and setup the install paths for the extension
     * @return void
     */
    abstract protected function setupInstallPaths();

    /**
     * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file
     * @return void
     * @throws  \RuntimeException
     */
    abstract protected function copyBaseFiles();

    /**
     * Method to store the extension to the database
     * @return void
     * @throws \RuntimeException
     */
    abstract protected function storeExtension();

    /**
     * Method to check if the extension is already present in the database
     * @return void
     * @throws \RuntimeException
     */
    protected function checkExistingExtension()
    {
        try {
            $this->currentExtensionId = $this->extension->findOne([
                'element' => $this->element,
                'type' => $this->type
            ]);
        } catch (\yii\db\Exception $e) {
            throw new \RuntimeException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Method to check if the extension is present in the filesystem, flags the route as update if so
     * @throws \yii\base\ErrorException
     */
    protected function checkExtensionInFilesystem()
    {
        if (file_exists($this->parent->getPath('extension_root')) && $this->currentExtensionId) {
            $updateElement = $this->manifest->update;
            if ($updateElement || $this->parent->isUpgrade()) {
                $this->set('route', 'update');
            }
        } else {
            FileHelper::removeDirectory($this->parent->getPath('extension_root'));
            if (!is_null($this->currentExtensionId)) {
                $this->currentExtensionId->delete();
            }
        }
    }

    /**
     * Method to create the extension root path if necessary
     * @throws yii\base\Exception
     */
    protected function createExtensionRoot()
    {
        $created = FileHelper::createDirectory($this->parent->getPath('extension_root'));
        if (!$created) {
            $this->parent->pushStep([
                'type' => 'folder',
                'path' => $this->parent->getPath('extension_root')
            ]);
        }
    }

    /**
     * Method to setup the update routine for the adapter
     * @return void
     */
    protected function setupUpdates()
    {
        // Some extensions may not have custom setup routines for updates
    }

    /**
     * Method to parse the queries specified in the `<sql>` tags
     * @return bool
     */
    protected function parseQueries()
    {
        if (in_array($this->route, ['install', 'uninstall'])) {
            return $this->doDatabaseTransactions();
        } elseif ($this->route === 'update') {
            if ($this->manifest->update) {
                return $this->parent->parseSchemaUpdates($this->manifest->update->schemas, $this->extension->id);
            }
        }
    }

    /**
     * Method to handle database transactions for the installer
     * @return boolean True on success
     */
    protected function doDatabaseTransactions()
    {
        if (isset($this->manifest->{$this->route}->sql)) {
            try {
                $this->parent->parseSQL($this->manifest->{$this->route}->sql);
            } catch (\yii\db\Exception $e) {
                throw new \RuntimeException(Yii::t('installer', 'ABORT_SQL_ERROR', [
                    'action' => ucfirst($this->route),
                    'error' => $e->getMessage()
                ]));
            }
            // If installing with success and there is an uninstall script, add an installer rollback step to rollback if needed
            if ($this->route === 'install' && isset($this->manifest->uninstall->sql)) {
                $this->parent->pushStep(['type' => 'query', 'script' => $this->manifest->uninstall->sql]);
            }
        }
        return true;
    }

    /**
     * Method to finalise the installation processing
     * @return void
     * @throws \RuntimeException
     */
    protected function finaliseInstall()
    {
    }

    /**
     * Method to parse optional tags in the manifest
     */
    protected function parseOptionalTags()
    {
        // Some extensions may not have optional tags
    }

    /**
     * Generic update method for extensions
     * @return bool
     * @throws \Throwable
     * @throws \yii\base\ErrorException
     * @throws \yii\db\Exception
     */
    public function update()
    {
        $this->set('route', 'update');
        return $this->install();
    }
}
